/*
 * Tencent is pleased to support the open source community by making spring-cloud-tencent available.
 *
 * Copyright (C) 2021 Tencent. All rights reserved.
 *
 * Licensed under the BSD 3-Clause License (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * https://opensource.org/licenses/BSD-3-Clause
 *
 * Unless required by applicable law or agreed to in writing, software distributed
 * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
 * CONDITIONS OF ANY KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations under the License.
 */

package com.tencent.cloud.polaris.config.adapter;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import com.tencent.cloud.common.util.EnvironmentUtils;
import com.tencent.cloud.plugin.tsf.tls.utils.SyncUtils;
import com.tencent.cloud.polaris.config.config.ConfigFileGroup;
import com.tencent.cloud.polaris.config.configdata.PolarisConfigDataLoader;
import com.tencent.cloud.polaris.context.config.PolarisContextProperties;
import com.tencent.cloud.polaris.context.config.extend.tsf.TsfTlsProperties;
import com.tencent.polaris.api.utils.ClassUtils;
import com.tencent.polaris.api.utils.CollectionUtils;
import com.tencent.polaris.api.utils.StringUtils;
import com.tencent.polaris.configuration.api.core.ConfigFileMetadata;
import com.tencent.polaris.configuration.api.core.ConfigFileService;
import com.tencent.polaris.configuration.client.internal.DefaultConfigFileMetadata;
import org.apache.commons.logging.Log;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.springframework.boot.logging.DeferredLogFactory;
import org.springframework.core.env.CompositePropertySource;
import org.springframework.core.env.Environment;
import org.springframework.core.env.MapPropertySource;

import static com.tencent.cloud.polaris.config.utils.PolarisPropertySourceUtils.loadGroupPolarisPropertySource;
import static com.tencent.cloud.polaris.config.utils.PolarisPropertySourceUtils.loadPolarisPropertySource;

/**
 * PolarisConfigFilePuller pull configFile from Polaris.
 *
 * @author wlx
 */
public final class PolarisConfigFilePuller {

	private static final Logger LOGGER = LoggerFactory.getLogger(PolarisConfigFilePuller.class);

	private static Log log;

	private PolarisContextProperties polarisContextProperties;

	private ConfigFileService configFileService;

	private PolarisConfigFilePuller() {
	}

	public static PolarisConfigFilePuller get(PolarisContextProperties polarisContextProperties, ConfigFileService configFileService) {
		return get(polarisContextProperties, configFileService, null);
	}

	/**
	 * Factory method to create PolarisConfigFilePuller for
	 * {@link PolarisConfigDataLoader},{@link PolarisConfigFileLocator}.
	 *
	 * @param polarisContextProperties     polarisContextProperties
	 * @param configFileService            configFileService
	 * @return PolarisConfigFilePuller instance
	 */
	public static PolarisConfigFilePuller get(PolarisContextProperties polarisContextProperties,
			ConfigFileService configFileService, DeferredLogFactory logFactory) {
		PolarisConfigFilePuller puller = new PolarisConfigFilePuller();
		puller.polarisContextProperties = polarisContextProperties;
		puller.configFileService = configFileService;
		if (logFactory != null) {
			log = logFactory.getLog(PolarisConfigFilePuller.class);
		}
		return puller;
	}

	private static void logInfo(String msg) {
		if (log != null) {
			log.info(msg);
		}
		else {
			LOGGER.info(msg);
		}
	}

	/**
	 * InitInternalConfigFiles for {@link PolarisConfigDataLoader}.
	 *
	 * @param compositePropertySource compositePropertySource
	 * @param activeProfiles          activeProfiles
	 * @param defaultProfiles         defaultProfiles
	 * @param serviceName             serviceName
	 */
	public void initInternalConfigFiles(CompositePropertySource compositePropertySource, String[] activeProfiles,
			String[] defaultProfiles, String serviceName) {
		logInfo("[SCT Config] Loading polaris internal config files");
		List<ConfigFileMetadata> internalConfigFiles = getInternalConfigFiles(activeProfiles, defaultProfiles, serviceName);
		for (ConfigFileMetadata configFile : internalConfigFiles) {
			PolarisPropertySource polarisPropertySource = loadPolarisPropertySource(configFileService,
					configFile.getNamespace(), configFile.getFileGroup(), configFile.getFileName());
			compositePropertySource.addPropertySource(polarisPropertySource);
			PolarisPropertySourceManager.addPropertySource(polarisPropertySource);
			logInfo(String.format("[SCT Config] Load and inject polaris internal config file success. file = %s", configFile));
		}
	}

	/**
	 * Init multiple CustomPolarisConfigFile.
	 *
	 * @param compositePropertySource compositePropertySource
	 * @param configFileGroups        configFileGroups
	 */
	public void initCustomPolarisConfigFiles(CompositePropertySource compositePropertySource,
			List<ConfigFileGroup> configFileGroups) {
		logInfo("[SCT Config] Loading polaris custom config files");
		configFileGroups.forEach(
				configFileGroup -> initCustomPolarisConfigFile(compositePropertySource, configFileGroup)
		);
	}

	/**
	 * Init single CustomPolarisConfigFile.
	 *
	 * @param compositePropertySource compositePropertySource
	 * @param configFileGroup         configFileGroup
	 */
	public void initCustomPolarisConfigFile(CompositePropertySource compositePropertySource,
			ConfigFileGroup configFileGroup) {
		String groupNamespace = configFileGroup.getNamespace();
		if (StringUtils.isBlank(groupNamespace)) {
			groupNamespace = polarisContextProperties.getNamespace();
		}
		String group = configFileGroup.getName();
		if (StringUtils.isBlank(group)) {
			throw new IllegalArgumentException("polaris config group name cannot be empty.");
		}
		List<String> files = configFileGroup.getFiles();
		if (CollectionUtils.isEmpty(files)) {
			PolarisPropertySource polarisPropertySource = loadGroupPolarisPropertySource(configFileService, groupNamespace, group);
			if (polarisPropertySource == null) {
				return;
			}
			compositePropertySource.addPropertySource(polarisPropertySource);
			PolarisPropertySourceManager.addPropertySource(polarisPropertySource);
			logInfo(String.format("[SCT Config] Load and inject polaris custom group config file success. namespace = %s, group = %s", groupNamespace, group));
		}
		else {
			for (String fileName : files) {
				PolarisPropertySource polarisPropertySource = loadPolarisPropertySource(configFileService, groupNamespace, group, fileName);
				compositePropertySource.addPropertySource(polarisPropertySource);
				PolarisPropertySourceManager.addPropertySource(polarisPropertySource);
				logInfo(String.format("[SCT Config] Load and inject polaris custom config file success. namespace = %s, group = %s, fileName = %s", groupNamespace, group, fileName));
			}
		}
	}

	/**
	 * Init TSF config groups.
	 * @param compositePropertySource compositePropertySource
	 */
	public void initTsfConfigGroups(CompositePropertySource compositePropertySource) {
		String tsfId = System.getProperty("tsf_id");
		String tsfNamespaceName = System.getProperty("tsf_namespace_name");
		String tsfGroupName = System.getProperty("tsf_group_name");

		if (StringUtils.isEmpty(tsfNamespaceName) || StringUtils.isEmpty(tsfGroupName)) {
			return;
		}
		logInfo("[SCT Config] Loading tsf config files");
		String namespace = polarisContextProperties.getNamespace();
		List<String> tsfConfigGroups = new ArrayList<>();
		tsfConfigGroups.add((StringUtils.isNotBlank(tsfId) ? tsfId + "." : "") + tsfGroupName + ".application_config_group");
		tsfConfigGroups.add((StringUtils.isNotBlank(tsfId) ? tsfId + "." : "") + tsfNamespaceName + ".global_config_group");

		if (EnvironmentUtils.isGateway()) {
			tsfConfigGroups.add((StringUtils.isNotBlank(tsfId) ? tsfId + "." : "") + tsfGroupName + ".gateway_config_group");
		}
		for (String tsfConfigGroup : tsfConfigGroups) {
			PolarisPropertySource polarisPropertySource = loadGroupPolarisPropertySource(configFileService, namespace, tsfConfigGroup);
			if (polarisPropertySource == null) {
				// not register to polaris
				continue;
			}
			compositePropertySource.addPropertySource(polarisPropertySource);
			PolarisPropertySourceManager.addPropertySource(polarisPropertySource);
			logInfo(String.format("[SCT Config] Load and inject tsf config file success. namespace = %s, group = %s", namespace, tsfConfigGroup));
		}
	}

	/**
	 * Init TSF TLS property source.
	 * @param compositePropertySource compositePropertySource
	 * @param tsfTlsProperties tsfTlsProperties
	 * @param environment environment
	 * @param serviceName serviceName
	 */
	public void initTsfTlsPropertySource(CompositePropertySource compositePropertySource,
			TsfTlsProperties tsfTlsProperties, Environment environment, String serviceName) {
		String address = getSystemPropertyOrEnv("MESH_CITADEL_ADDR");
		if (StringUtils.isNotBlank(address)
				&& (StringUtils.equals("tsf", System.getProperty("server.ssl.bundle"))
				|| (environment != null && StringUtils.equals("tsf", environment.getProperty("server.ssl.bundle")))
				|| "tsf".equals(compositePropertySource.getProperty(("server.ssl.bundle"))))
				&& ClassUtils.isClassPresent("com.tencent.cloud.plugin.tsf.tls.utils.SyncUtils")
				&& !SyncUtils.isInitialized()) {
			// get certPath
			String certPath = getSystemPropertyOrEnv("MESH_CITADEL_CERT");
			// get token
			String token = getSystemPropertyOrEnv("tsf_token");
			// get validityDuration
			Object validityDuration = compositePropertySource.getProperty("spring.cloud.polaris.tls.validityDuration");
			if (validityDuration == null) {
				if (environment != null) {
					validityDuration = environment.getProperty("spring.cloud.polaris.tls.validityDuration",
							Long.class, TsfTlsProperties.DEFAULT_VALIDITY_DURATION);
				}
				else if (tsfTlsProperties != null) {
					validityDuration = tsfTlsProperties.getValidityDuration();
				}
				else {
					validityDuration = TsfTlsProperties.DEFAULT_VALIDITY_DURATION;
				}
			}
			if (validityDuration instanceof String) {
				validityDuration = Long.valueOf((String) validityDuration);
			}
			// get refreshBefore
			Object refreshBefore = compositePropertySource.getProperty("spring.cloud.polaris.tls.refreshBefore");
			if (refreshBefore == null) {
				if (environment != null) {
					refreshBefore = environment.getProperty("spring.cloud.polaris.tls.refreshBefore",
							Long.class, TsfTlsProperties.DEFAULT_REFRESH_BEFORE);
				}
				else if (tsfTlsProperties != null) {
					refreshBefore = tsfTlsProperties.getRefreshBefore();
				}
				else {
					refreshBefore = TsfTlsProperties.DEFAULT_REFRESH_BEFORE;
				}
			}
			if (refreshBefore instanceof String) {
				refreshBefore = Long.valueOf((String) refreshBefore);
			}
			// get watchInterval
			Object watchInterval = compositePropertySource.getProperty("spring.cloud.polaris.tls.watchInterval");
			if (watchInterval == null) {
				if (environment != null) {
					watchInterval = environment.getProperty("spring.cloud.polaris.tls.watchInterval",
							Long.class, TsfTlsProperties.DEFAULT_WATCH_INTERVAL);
				}
				else if (tsfTlsProperties != null) {
					watchInterval = tsfTlsProperties.getWatchInterval();
				}
				else {
					watchInterval = TsfTlsProperties.DEFAULT_WATCH_INTERVAL;
				}
			}
			if (watchInterval instanceof String) {
				watchInterval = Long.valueOf((String) watchInterval);
			}
			SyncUtils.init(serviceName, address, certPath, token, (Long) validityDuration, (Long) refreshBefore, (Long) watchInterval);
			if (SyncUtils.isVerified()) {
				Map<String, Object> tlsEnvProperties = new HashMap<>();
				// set ssl
				Object clientAuth = getPropertyWithDefault(compositePropertySource, environment,
						"server.ssl.client-auth", "want");
				tlsEnvProperties.put("server.ssl.client-auth", clientAuth);
				Object protocol = getPropertyWithDefault(compositePropertySource, environment,
						"spring.cloud.polaris.discovery.protocol", "https");
				tlsEnvProperties.put("spring.cloud.polaris.discovery.protocol", protocol);
				tlsEnvProperties.put("tsf.discovery.scheme", protocol);

				// set tsf spring ssl bundle
				tlsEnvProperties.put("spring.ssl.bundle.pem.tsf.reload-on-update", "true");
				if (StringUtils.isNotBlank(SyncUtils.getPemKeyStoreCertPath()) && StringUtils.isNotBlank(SyncUtils.getPemKeyStoreKeyPath())) {
					tlsEnvProperties.put("spring.ssl.bundle.pem.tsf.keystore.certificate", SyncUtils.getPemKeyStoreCertPath());
					tlsEnvProperties.put("spring.ssl.bundle.pem.tsf.keystore.private-key", SyncUtils.getPemKeyStoreKeyPath());
				}
				if (StringUtils.isNotBlank(SyncUtils.getPemTrustStoreCertPath())) {
					tlsEnvProperties.put("spring.ssl.bundle.pem.tsf.truststore.certificate", SyncUtils.getPemTrustStoreCertPath());
				}

				// process environment
				MapPropertySource propertySource = new MapPropertySource("tsf-tls-config", tlsEnvProperties);
				compositePropertySource.addPropertySource(propertySource);
			}
		}
	}

	private String getSystemPropertyOrEnv(String key) {
		return System.getProperty(key, System.getenv(key));
	}

	private Object getPropertyWithDefault(CompositePropertySource compositePropertySource,
			Environment environment, String propertyKey, String defaultValue) {
		Object value = compositePropertySource.getProperty(propertyKey);
		if (value == null) {
			if (environment != null) {
				value = environment.getProperty(propertyKey, defaultValue);
			}
			else {
				value = System.getProperty(propertyKey, defaultValue);
			}
		}
		return value;
	}

	private List<ConfigFileMetadata> getInternalConfigFiles(
			String[] activeProfiles, String[] defaultProfiles, String serviceName) {
		String namespace = polarisContextProperties.getNamespace();
		if (StringUtils.isNotBlank(polarisContextProperties.getService())) {
			serviceName = polarisContextProperties.getService();
		}
		// priority: application-${profile} > application > boostrap-${profile} > boostrap
		return getInternalConfigFiles(activeProfiles, defaultProfiles, namespace, serviceName);
	}

	private List<ConfigFileMetadata> getInternalConfigFiles(
			String[] activeProfiles, String[] defaultProfiles, String namespace, String serviceName) {
		List<String> profileList = new ArrayList<>();
		if (CollectionUtils.isNotEmpty(activeProfiles)) {
			profileList.addAll(Arrays.asList(activeProfiles));
		}
		else if (CollectionUtils.isNotEmpty(defaultProfiles)) {
			profileList.addAll(Arrays.asList(defaultProfiles));
		}

		List<ConfigFileMetadata> internalConfigFiles = new LinkedList<>();
		// build application config files
		buildInternalApplicationConfigFiles(internalConfigFiles, namespace, serviceName, profileList);
		// build bootstrap config files
		buildInternalBootstrapConfigFiles(internalConfigFiles, namespace, serviceName, profileList);

		return internalConfigFiles;
	}

	private void buildInternalApplicationConfigFiles(
			List<ConfigFileMetadata> internalConfigFiles, String namespace, String serviceName, List<String> profiles) {
		for (String profile : profiles) {
			if (StringUtils.isBlank(profile)) {
				continue;
			}
			internalConfigFiles.add(new DefaultConfigFileMetadata(namespace, serviceName, "application-" + profile + ".properties"));
			internalConfigFiles.add(new DefaultConfigFileMetadata(namespace, serviceName, "application-" + profile + ".yml"));
			internalConfigFiles.add(new DefaultConfigFileMetadata(namespace, serviceName, "application-" + profile + ".yaml"));
		}
		// build default config properties files.
		internalConfigFiles.add(new DefaultConfigFileMetadata(namespace, serviceName, "application.properties"));
		internalConfigFiles.add(new DefaultConfigFileMetadata(namespace, serviceName, "application.yml"));
		internalConfigFiles.add(new DefaultConfigFileMetadata(namespace, serviceName, "application.yaml"));
	}

	private void buildInternalBootstrapConfigFiles(
			List<ConfigFileMetadata> internalConfigFiles, String namespace, String serviceName, List<String> profiles) {
		for (String profile : profiles) {
			if (StringUtils.isBlank(profile)) {
				continue;
			}
			internalConfigFiles.add(new DefaultConfigFileMetadata(namespace, serviceName, "bootstrap-" + profile + ".properties"));
			internalConfigFiles.add(new DefaultConfigFileMetadata(namespace, serviceName, "bootstrap-" + profile + ".yml"));
			internalConfigFiles.add(new DefaultConfigFileMetadata(namespace, serviceName, "bootstrap-" + profile + ".yaml"));
		}
		// build default config properties files.
		internalConfigFiles.add(new DefaultConfigFileMetadata(namespace, serviceName, "bootstrap.properties"));
		internalConfigFiles.add(new DefaultConfigFileMetadata(namespace, serviceName, "bootstrap.yml"));
		internalConfigFiles.add(new DefaultConfigFileMetadata(namespace, serviceName, "bootstrap.yaml"));
	}
}
